<?php

namespace app\socket\object;

use ArrayAccess;
use Closure;
use InvalidArgumentException;
use JsonSerializable;
use think\Cache;
use think\helper\Arr;
use think\helper\Str;

class Game implements JsonSerializable, ArrayAccess
{
    use \think\model\concern\TimeStamp;

    /**
     * 缓存标识
     *
     * @var mixed
     * @author 秋月 414111907@qq.com
     */
    public $pk = "";
    /**
     * 缓存前缀
     *
     * @var array
     * @author 秋月 414111907@qq.com
     */
    public $cacheKey = [];



    /**
     * 字段自动类型转换
     * @var array
     */
    protected $type = [];


    /**
     * 数据表只读字段
     * @var array
     */
    protected $readonly = [];

    /**
     * 当前模型数据
     * @var array
     */
    private $data = [];

    /**
     * 原始数据
     * @var array
     */
    private $origin = [];

    /**
     * JSON数据表字段
     * @var array
     */
    protected $json = [];

    /**
     * JSON数据表字段类型
     * @var array
     */
    protected $jsonType = [];

    /**
     * JSON数据取出是否需要转换为数组
     * @var bool
     */
    protected $jsonAssoc = false;

    /**
     * 是否严格字段大小写
     * @var bool
     */
    protected $strict = true;

    /**
     * 修改器执行记录
     * @var array
     */
    private $set = [];

    /**
     * 动态获取器
     * @var array
     */
    private $withAttr = [];


    /**
     * 数据输出显示的属性
     * @var array
     */
    protected $visible = [];

    /**
     * 数据输出隐藏的属性
     * @var array
     */
    protected $hidden = [];

    /**
     * 数据输出需要追加的属性
     * @var array
     */
    protected $append = [];




    /**
     * 获取缓存类 使用redis
     *
     * @return \Redis
     * @author 秋月 414111907@qq.com
     */
    public function getRedisAttr()
    {
        return app(Cache::class);
    }


    /**
     * 根据标识从缓存中创建
     *
     * @param int $fd
     * @return $this
     * @author 秋月 414111907@qq.com
     */
    public static function find($fd)
    {
        $class = get_called_class();
        $obj = new $class;
        $key = implode(":", array_merge($obj->cacheKey, [$fd]));
        $data = $obj->redis->hgetall($key);
        $obj->data($data);
        return $obj;
    }

    /**
     * 将对象保存到缓存中
     *
     * @return bool
     * @author 秋月 414111907@qq.com
     */
    public function save()
    {
        $key = implode(":", array_merge($this->cacheKey, [$this[$this->pk]]));
        $data = $this->toArray();

        foreach ($this->type as $field => $type) {
            if (Arr::has($data, $field)) {
                $data[$field] =  $this->writeTransform($data[$field], $type);
            }
        }
        return $this->redis->hmset($key, $data);
    }

    /**
     * 从缓存中删除
     *
     * @return bool
     * @author 秋月 414111907@qq.com
     */
    public function delete()
    {
        $key = implode(":", array_merge($this->cacheKey, [$this[$this->pk]]));
        return $this->redis->del($key);
    }









    /**
     * 设置允许写入的字段
     * @access public
     * @param  array $field 允许写入的字段
     * @return $this
     */
    public function allowField(array $field)
    {
        $this->field = $field;

        return $this;
    }

    /**
     * 设置只读字段
     * @access public
     * @param  array $field 只读字段
     * @return $this
     */
    public function readOnly(array $field)
    {
        $this->readonly = $field;

        return $this;
    }

    /**
     * 获取实际的字段名
     * @access protected
     * @param  string $name 字段名
     * @return string
     */
    protected function getRealFieldName(string $name): string
    {
        return $this->strict ? $name : Str::snake($name);
    }

    /**
     * 设置数据对象值
     * @access public
     * @param  array    $data  数据
     * @param  bool     $set   是否调用修改器
     * @param  array    $allow 允许的字段名
     * @return $this
     */
    public function data(array $data, bool $set = false, array $allow = [])
    {
        // 清空数据
        $this->data = [];


        if (!empty($allow)) {
            $result = [];
            foreach ($allow as $name) {
                if (isset($data[$name])) {
                    $result[$name] = $data[$name];
                }
            }
            $data = $result;
        }

        if ($set) {
            // 数据对象赋值
            $this->setAttrs($data);
        } else {
            $this->data = $data;
        }

        return $this;
    }

    /**
     * 批量追加数据对象值
     * @access public
     * @param  array $data  数据
     * @param  bool  $set   是否需要进行数据处理
     * @return $this
     */
    public function appendData(array $data, bool $set = false)
    {
        if ($set) {
            $this->setAttrs($data);
        } else {
            $this->data = array_merge($this->data, $data);
        }

        return $this;
    }

    /**
     * 获取对象原始数据 如果不存在指定字段返回null
     * @access public
     * @param  string $name 字段名 留空获取全部
     * @return mixed
     */
    public function getOrigin(string $name = null)
    {
        if (is_null($name)) {
            return $this->origin;
        }

        return array_key_exists($name, $this->origin) ? $this->origin[$name] : null;
    }

    /**
     * 获取对象原始数据 如果不存在指定字段返回false
     * @access public
     * @param  string $name 字段名 留空获取全部
     * @return mixed
     * @throws InvalidArgumentException
     */
    public function getData(string $name = null)
    {
        if (is_null($name)) {
            return $this->data;
        }

        $fieldName = $this->getRealFieldName($name);

        if (array_key_exists($fieldName, $this->data)) {
            return $this->data[$fieldName];
        }

        throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
    }

    /**
     * 获取变化的数据 并排除只读数据
     * @access public
     * @return array
     */
    public function getChangedData(): array
    {
        $data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
            if ((empty($a) || empty($b)) && $a !== $b) {
                return 1;
            }

            return is_object($a) || $a != $b ? 1 : 0;
        });

        // 只读字段不允许更新
        foreach ($this->readonly as $key => $field) {
            if (isset($data[$field])) {
                unset($data[$field]);
            }
        }

        return $data;
    }

    /**
     * 直接设置数据对象值
     * @access public
     * @param  string $name  属性名
     * @param  mixed  $value 值
     * @return void
     */
    public function set(string $name, $value): void
    {
        $name = $this->getRealFieldName($name);

        $this->data[$name] = $value;
    }

    /**
     * 通过修改器 批量设置数据对象值
     * @access public
     * @param  array $data  数据
     * @return void
     */
    public function setAttrs(array $data): void
    {
        // 进行数据处理
        foreach ($data as $key => $value) {
            $this->setAttr($key, $value, $data);
        }
    }

    /**
     * 通过修改器 设置数据对象值
     * @access public
     * @param  string $name  属性名
     * @param  mixed  $value 属性值
     * @param  array  $data  数据
     * @return void
     */
    public function setAttr(string $name, $value, array $data = []): void
    {
        $name = $this->getRealFieldName($name);

        if (isset($this->set[$name])) {
            return;
        }

        // 检测修改器
        $method = 'set' . Str::studly($name) . 'Attr';

        if (method_exists($this, $method)) {
            $array = $this->data;

            $value = $this->$method($value, array_merge($this->data, $data));

            $this->set[$name] = true;
            if (is_null($value) && $array !== $this->data) {
                return;
            }
        } elseif (isset($this->type[$name])) {
            // 类型转换
            $value = $this->writeTransform($value, $this->type[$name]);
        }

        // 设置数据对象属性
        $this->data[$name] = $value;
    }

    public function withAttr($field, Closure $next)
    {
        return $this->withAttribute($field, $next);
    }

    /**
     * 数据写入 类型转换
     * @access protected
     * @param  mixed        $value 值
     * @param  string|array $type  要转换的类型
     * @return mixed
     */
    protected function writeTransform($value, $type)
    {
        if (is_null($value)) {
            return;
        }

        if (is_array($type)) {
            list($type, $param) = $type;
        } elseif (strpos($type, ':')) {
            list($type, $param) = explode(':', $type, 2);
        }

        switch ($type) {
            case 'integer':
                $value = (int) $value;
                break;
            case 'float':
                if (empty($param)) {
                    $value = (float) $value;
                } else {
                    $value = (float) number_format($value, $param, '.', '');
                }
                break;
            case 'boolean':
                $value = (bool) $value;
                break;
            case 'timestamp':
                if (!is_numeric($value)) {
                    $value = strtotime($value);
                }
                break;
            case 'datetime':
                $value = is_numeric($value) ? $value : strtotime($value);
                $value = $this->formatDateTime('Y-m-d H:i:s.u', $value);
                break;
            case 'object':
                if (is_object($value)) {
                    $value = json_encode($value, JSON_FORCE_OBJECT);
                }
                break;
            case 'array':
                $value = (array) $value;
            case 'json':
                $option = !empty($param) ? (int) $param : JSON_UNESCAPED_UNICODE;
                $value = json_encode($value, $option);
                break;
            case 'serialize':
                $value = serialize($value);
                break;
            default:
                if (is_object($value) && false !== strpos($type, '\\') && method_exists($value, '__toString')) {
                    // 对象类型
                    $value = $value->__toString();
                }
        }

        return $value;
    }

    /**
     * 获取器 获取数据对象的值
     * @access public
     * @param  string $name 名称
     * @return mixed
     * @throws InvalidArgumentException
     */
    public function getAttr(string $name)
    {

        try {
            $relation = false;
            $value = $this->getData($name);
        } catch (InvalidArgumentException $e) {
            $value = null;
        }

        return $this->getValue($name, $value, $relation);
    }

    /**
     * 获取经过获取器处理后的数据对象的值
     * @access protected
     * @param  string      $name 字段名称
     * @param  mixed       $value 字段值
     * @param  bool|string $relation 是否为关联属性或者关联名
     * @return mixed
     * @throws InvalidArgumentException
     */
    protected function getValue(string $name, $value)
    {
        // 检测属性获取器
        $fieldName = $this->getRealFieldName($name);
        $method = 'get' . Str::studly($name) . 'Attr';

        if (isset($this->withAttr[$fieldName])) {
            if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
                $value = $this->getJsonValue($fieldName, $value);
            } else {
                $closure = $this->withAttr[$fieldName];
                $value = $closure($value, $this->data);
            }
        } elseif (method_exists($this, $method)) {

            $value = $this->$method($value, $this->data);
        } elseif (isset($this->type[$fieldName])) {
            // 类型转换
            $value = $this->readTransform($value, $this->type[$fieldName]);
        }

        return $value;
    }

    /**
     * 获取JSON字段属性值
     * @access protected
     * @param  string $name  属性名
     * @param  mixed  $value JSON数据
     * @return mixed
     */
    protected function getJsonValue($name, $value)
    {
        foreach ($this->withAttr[$name] as $key => $closure) {
            if ($this->jsonAssoc) {
                $value[$key] = $closure($value[$key], $value);
            } else {
                $value->$key = $closure($value->$key, $value);
            }
        }

        return $value;
    }

    /**
     * 数据读取 类型转换
     * @access protected
     * @param  mixed        $value 值
     * @param  string|array $type  要转换的类型
     * @return mixed
     */
    protected function readTransform($value, $type)
    {
        if (is_null($value)) {
            return;
        }

        if (is_array($type)) {
            list($type, $param) = $type;
        } elseif (strpos($type, ':')) {
            list($type, $param) = explode(':', $type, 2);
        }

        switch ($type) {
            case 'integer':
                $value = (int) $value;
                break;
            case 'float':
                if (empty($param)) {
                    $value = (float) $value;
                } else {
                    $value = (float) number_format($value, $param, '.', '');
                }
                break;
            case 'boolean':
                $value = (bool) $value;
                break;
            case 'timestamp':
                if (!is_null($value)) {
                    $format = !empty($param) ? $param : $this->dateFormat;
                    $value = $this->formatDateTime($format, $value, true);
                }
                break;
            case 'datetime':
                if (!is_null($value)) {
                    $format = !empty($param) ? $param : $this->dateFormat;
                    $value = $this->formatDateTime($format, $value);
                }
                break;
            case 'json':
                $value = json_decode($value, true);
                break;
            case 'array':
                $value = empty($value) ? [] : json_decode($value, true);
                break;
            case 'object':
                $value = empty($value) ? new \stdClass() : json_decode($value);
                break;
            case 'serialize':
                try {
                    $value = unserialize($value);
                } catch (\Exception $e) {
                    $value = null;
                }
                break;
            default:
                if (false !== strpos($type, '\\')) {
                    // 对象类型
                    $value = new $type($value);
                }
        }
        return $value;
    }

    /**
     * 设置数据字段获取器
     * @access public
     * @param  string|array $name       字段名
     * @param  callable     $callback   闭包获取器
     * @return $this
     */
    public function withAttribute($name, callable $callback = null)
    {
        if (is_array($name)) {
            foreach ($name as $key => $val) {
                $this->withAttribute($key, $val);
            }
        } else {
            $name = $this->getRealFieldName($name);

            if (strpos($name, '.')) {
                list($name, $key) = explode('.', $name);

                $this->withAttr[$name][$key] = $callback;
            } else {
                $this->withAttr[$name] = $callback;
            }
        }

        return $this;
    }

    /**
     * 设置需要附加的输出属性
     * @access public
     * @param  array $append   属性列表
     * @return $this
     */
    public function append(array $append = [])
    {
        $this->append = $append;

        return $this;
    }

    /**
     * 设置需要隐藏的输出属性
     * @access public
     * @param  array $hidden   属性列表
     * @return $this
     */
    public function hidden(array $hidden = [])
    {
        $this->hidden = $hidden;

        return $this;
    }

    /**
     * 设置需要输出的属性
     * @access public
     * @param  array $visible
     * @return $this
     */
    public function visible(array $visible = [])
    {
        $this->visible = $visible;

        return $this;
    }

    /**
     * 转换当前模型对象为数组
     * @access public
     * @return array
     */
    public function toArray(): array
    {
        $item = [];
        $hasVisible = false;

        foreach ($this->visible as $key => $val) {
            if (is_string($val)) {
                if (strpos($val, '.')) {
                    list($relation, $name) = explode('.', $val);
                    $this->visible[$relation][] = $name;
                } else {
                    $this->visible[$val] = true;
                    $hasVisible = true;
                }
                unset($this->visible[$key]);
            }
        }

        foreach ($this->hidden as $key => $val) {
            if (is_string($val)) {
                if (strpos($val, '.')) {
                    list($relation, $name) = explode('.', $val);
                    $this->hidden[$relation][] = $name;
                } else {
                    $this->hidden[$val] = true;
                }
                unset($this->hidden[$key]);
            }
        }

        // 合并关联数据
        $data = $this->data;

        foreach ($data as $key => $val) {
            if (isset($this->visible[$key])) {
                $item[$key] = $this->getAttr($key);
            } elseif (!isset($this->hidden[$key]) && !$hasVisible) {
                $item[$key] = $this->getAttr($key);
            }
        }

        // 追加属性（必须定义获取器）
        foreach ($this->append as $key => $name) {
            $this->appendAttrToArray($item, $key, $name);
        }

        return $item;
    }

    protected function appendAttrToArray(array &$item, $key, $name)
    {
        $value = $this->getAttr($name);
        $item[$name] = $value;
    }

    /**
     * 转换当前模型对象为JSON字符串
     * @access public
     * @param  integer $options json参数
     * @return string
     */
    public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
    {
        return json_encode($this->toArray(), $options);
    }

    public function __toString()
    {
        return $this->toJson();
    }

    // JsonSerializable
    public function jsonSerialize()
    {
        return $this->toArray();
    }

    public function __set(string $name, $value): void
    {
        $this->setAttr($name, $value);
    }

    /**
     * 获取器 获取数据对象的值
     * @access public
     * @param string $name 名称
     * @return mixed
     */
    public function __get(string $name)
    {
        return $this->getAttr($name);
    }

    /**
     * 销毁数据对象的值
     * @access public
     * @param string $name 名称
     * @return void
     */
    public function __unset(string $name): void
    {
        unset($this->data[$name], $this->relation[$name]);
    }

    // ArrayAccess
    public function offsetSet($name, $value)
    {
        $this->setAttr($name, $value);
    }

    public function offsetExists($name): bool
    {
        return $this->__isset($name);
    }

    public function offsetUnset($name)
    {
        $this->__unset($name);
    }

    public function offsetGet($name)
    {
        return $this->getAttr($name);
    }
    /**
     * 检测数据对象的值
     * @access public
     * @param string $name 名称
     * @return bool
     */
    public function __isset(string $name): bool
    {
        return !is_null($this->getAttr($name));
    }
}
